package com.stars.easyms.redis.interceptor;

import com.stars.easyms.base.util.ConverterUtil;
import com.stars.easyms.base.util.MessageFormatUtil;
import com.stars.easyms.base.util.TimeUtil;
import com.stars.easyms.redis.annotion.*;
import com.stars.easyms.redis.bean.RedisEvalMap;
import com.stars.easyms.redis.bean.RedisIndexMap;
import com.stars.easyms.redis.exception.RedisInterceptorException;
import com.stars.easyms.redis.exception.RedisRuntimeException;
import com.stars.easyms.redis.initializer.RedisManagerInitializer;
import com.stars.easyms.redis.template.EasyMsRedisTemplate;
import com.stars.easyms.base.util.GenericTypeUtil;
import lombok.Setter;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * <p>className: AbstractRedisInterceptor</p>
 * <p>description: 抽象Redis注解拦截器</p>
 *
 * @author guoguifang
 * @version 1.2.1
 * @date 2019-03-18 13:52
 */
abstract class AbstractRedisInterceptor<T extends Annotation> implements MethodInterceptor {

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Setter
    private RedisManagerInitializer redisManagerInitializer;

    @Setter
    protected EasyMsRedisTemplate easyMsRedisTemplate;

    @Setter
    private Class<T> classAnnotationType;

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws RedisInterceptorException {
        Method method = methodInvocation.getMethod();
        if (!method.isAnnotationPresent(classAnnotationType)) {
            return proceed(methodInvocation);
        }
        return this.invoke(methodInvocation, method.getAnnotation(classAnnotationType));
    }

    /**
     * 执行拦截操作
     */
    abstract Object invoke(MethodInvocation methodInvocation, T annotation) throws RedisInterceptorException;

    /**
     * 判断redis模块是否激活
     */
    boolean isEnabled() {
        if (redisManagerInitializer != null && easyMsRedisTemplate != null) {
            if (!redisManagerInitializer.isInit()) {
                redisManagerInitializer.initRedis();
            }
            return true;
        }
        return false;
    }

    /**
     * 获取加了RedisKey注解的参数值
     */
    String getRedisKey(MethodInvocation methodInvocation, String group) {
        Method pointMethod = methodInvocation.getMethod();
        // 获取方法参数的注解
        Annotation[][] argumentAnnotationArray = pointMethod.getParameterAnnotations();
        // 获取所有的加了RedisKey注解的值,如果存在RedisKey则redis中保存的key值为redisKeyPrefix:redisKey的格式，如不存在则则redis中保存的key值为redisKeyPrefix
        List<String> redisKeys = new ArrayList<>();
        redisKeys.add(group);
        for (int index = 0; index < argumentAnnotationArray.length; index++) {
            Annotation[] argumentAnnotations = argumentAnnotationArray[index];
            for (Annotation argumentAnnotation : argumentAnnotations) {
                if (argumentAnnotation.annotationType().equals(RedisKey.class)) {
                    Object argument = methodInvocation.getArguments()[index];
                    if (argument != null) {
                        redisKeys.add(ConverterUtil.cast(argument, String.class));
                    }
                    break;
                }
            }
        }
        return easyMsRedisTemplate.getRedisKeyWithPrefix(redisKeys.toArray(new String[0]));
    }

    Long getExpire(MethodInvocation methodInvocation, String defaultExpireStr) {
        Long expire = getExpire(methodInvocation);
        if (expire == null) {
            expire = TimeUtil.parseToMilliseconds(defaultExpireStr);
        }
        return expire;
    }

    /**
     * 获取过期时间
     */
    Long getExpire(MethodInvocation methodInvocation) {
        Method pointMethod = methodInvocation.getMethod();
        Annotation[][] argumentAnnotationArray = pointMethod.getParameterAnnotations();
        for (int index = 0; index < argumentAnnotationArray.length; index++) {
            Annotation[] argumentAnnotations = argumentAnnotationArray[index];
            for (Annotation argumentAnnotation : argumentAnnotations) {
                if (argumentAnnotation.annotationType().equals(RedisExpireAt.class)) {
                    Object argument = methodInvocation.getArguments()[index];
                    if (argument instanceof Long) {
                        long expireMilliseconds = (Long) argument - System.currentTimeMillis();
                        return expireMilliseconds > 0 ? expireMilliseconds : null;
                    } else if (argument instanceof Integer) {
                        long expireMilliseconds = (Integer) argument * 1000 - System.currentTimeMillis();
                        return expireMilliseconds > 0 ? expireMilliseconds : null;
                    } else if (argument instanceof Date) {
                        long expireMilliseconds = ((Date) argument).getTime() - System.currentTimeMillis();
                        return expireMilliseconds > 0 ? expireMilliseconds : null;
                    }
                    break;
                } else if (argumentAnnotation.annotationType().equals(RedisExpire.class)) {
                    Object argument = methodInvocation.getArguments()[index];
                    if (argument instanceof Long) {
                        Long expireMilliseconds = (Long) argument;
                        return expireMilliseconds > 0 ? expireMilliseconds : null;
                    } else if (argument instanceof Integer) {
                        Integer expireSeconds = (Integer) argument;
                        return expireSeconds > 0 ? (long) expireSeconds * 1000 : null;
                    } else if (argument instanceof String) {
                        Long expireMilliseconds = TimeUtil.parseToMilliseconds((String) argument);
                        return expireMilliseconds != null && expireMilliseconds > 0 ? expireMilliseconds : null;
                    }
                    break;
                }
            }
        }
        return null;
    }

    void resetExpire(MethodInvocation methodInvocation, String redisKey, boolean resetExpire,
                     long currRemainingExpireMills, String defaultExpireStr) {
        // 1.如果需要重置过期时间：优化获取完整过期时间的值，如果为空或小于等于0则判断原剩余时间是否大于0，若大于0则使用原剩余时间作为过期时间
        // 2.如果不需要重置过期时间：当原剩余时间小于等于0时获取完整过期时间的值
        if (resetExpire || currRemainingExpireMills <= 0) {
            Long expire = getExpire(methodInvocation, defaultExpireStr);
            if (expire != null && expire > 0) {
                easyMsRedisTemplate.expire(redisKey, expire, TimeUnit.MILLISECONDS);
            } else if (currRemainingExpireMills > 0) {
                easyMsRedisTemplate.expire(redisKey, currRemainingExpireMills, TimeUnit.MILLISECONDS);
            }
        } else {
            easyMsRedisTemplate.expire(redisKey, currRemainingExpireMills, TimeUnit.MILLISECONDS);
        }
    }

    /**
     * 获取加了RedisHashKey注解的参数值
     */
    @SuppressWarnings("unchecked")
    Set<String> getRedisHashKey(MethodInvocation methodInvocation) {
        Method pointMethod = methodInvocation.getMethod();
        // 获取方法参数的注解
        Annotation[][] argumentAnnotationArray = pointMethod.getParameterAnnotations();
        // 获取方法的输入参数类型
        Class<?>[] argumentTypes = pointMethod.getParameterTypes();
        // 获取所有的加了RedisField注解的值,RedisField只在RedisType.HASH类型中存在
        String redisHashKeyException = "Class(" + pointMethod.getDeclaringClass().getName() + ") method("
                + pointMethod.getName() + ") annotation(RedisHashKey) parameter type must be String or Collection<String> or Array<String>!";
        Set<String> fieldSet = new HashSet<>(16);
        for (int index = 0; index < argumentAnnotationArray.length; index++) {
            Annotation[] argumentAnnotations = argumentAnnotationArray[index];
            for (Annotation argumentAnnotation : argumentAnnotations) {
                if (argumentAnnotation.annotationType().equals(RedisHashKey.class)) {
                    Object argument = methodInvocation.getArguments()[index];
                    if (argument != null) {
                        if (String.class.isAssignableFrom(argumentTypes[index])) {
                            fieldSet.add((String) argument);
                        } else if (argumentTypes[index].isArray()) {
                            if (String.class.isAssignableFrom(argumentTypes[index].getComponentType())) {
                                for (String argumentItem : (String[]) argument) {
                                    if (argumentItem != null) {
                                        fieldSet.add(argumentItem);
                                    }
                                }
                            } else {
                                throw new RedisRuntimeException(redisHashKeyException);
                            }
                        } else if (Collection.class.isAssignableFrom(argumentTypes[index])) {
                            if (String.class.isAssignableFrom(
                                    GenericTypeUtil.getGenericClass(pointMethod.getGenericParameterTypes()[index], 0))) {
                                fieldSet.addAll((Collection<String>) argument);
                            } else {
                                throw new RedisRuntimeException(redisHashKeyException);
                            }
                        } else {
                            throw new RedisRuntimeException(redisHashKeyException);
                        }
                    } else {
                        throw new RedisRuntimeException(MessageFormatUtil.format("Class({}) method({}) "
                                        + "parameter({}) value is null, annotation(RedisHashKey) value must be not null!",
                                pointMethod.getDeclaringClass().getName(), pointMethod.getName(),
                                pointMethod.getParameters()[index].getName()));
                    }
                    break;
                }
            }
        }
        return fieldSet;
    }

    /**
     * 获取加了RedisEvalKey、RedisEvalArg注解的参数值
     */
    @SuppressWarnings("unchecked")
    RedisEvalMap getRedisEvalMap(MethodInvocation methodInvocation) {
        Method pointMethod = methodInvocation.getMethod();
        // 获取方法参数的注解
        Annotation[][] argumentAnnotationArray = pointMethod.getParameterAnnotations();
        // 获取方法的输入参数类型
        Class<?>[] argumentTypes = pointMethod.getParameterTypes();
        // 注解RedisEvalKey、RedisEvalArg每个方法只能有一个并且每个注解的类型必须是List<String>
        String redisEvalException = "Class(" + pointMethod.getDeclaringClass().getName() + ") method("
                + pointMethod.getName() + ") annotation(RedisEvalKey) and annotation(RedisEvalArg) each must " +
                "be unique and paramter type must be List<String>!";
        List<String> redisEvalKey = null;
        List<String> redisEvalArg = null;
        for (int index = 0; index < argumentAnnotationArray.length; index++) {
            Annotation[] argumentAnnotations = argumentAnnotationArray[index];
            for (Annotation argumentAnnotation : argumentAnnotations) {
                Class<?> argumentAnnotationType = argumentAnnotation.annotationType();
                if (argumentAnnotationType.equals(RedisEvalKey.class) || argumentAnnotationType.equals(RedisEvalArg.class)) {
                    boolean isInvalidAnnotation = (argumentAnnotationType.equals(RedisEvalKey.class)
                            && redisEvalKey != null) || (argumentAnnotationType.equals(RedisEvalArg.class) && redisEvalArg != null);
                    if (isInvalidAnnotation) {
                        throw new RedisRuntimeException(redisEvalException);
                    }

                    Object argument = methodInvocation.getArguments()[index];
                    if (argument != null) {
                        if (List.class.isAssignableFrom(argumentTypes[index])
                                && String.class.isAssignableFrom(GenericTypeUtil.getGenericClass(pointMethod.getGenericParameterTypes()[index], 0))) {
                            if (argumentAnnotationType.equals(RedisEvalKey.class)) {
                                redisEvalKey = (List<String>) argument;
                            } else {
                                redisEvalArg = (List<String>) argument;
                            }
                        } else {
                            throw new RedisRuntimeException(redisEvalException);
                        }
                    }
                    break;
                }
            }
        }
        return new RedisEvalMap(redisEvalKey, redisEvalArg);
    }

    /**
     * 获取加了RedisIndexStart、RedisIndexEnd注解的参数值
     */
    RedisIndexMap getRedisIndexMap(MethodInvocation methodInvocation) {
        Method pointMethod = methodInvocation.getMethod();
        // 获取方法参数的注解
        Annotation[][] argumentAnnotationArray = pointMethod.getParameterAnnotations();
        // 获取方法的输入参数类型
        Class<?>[] argumentTypes = pointMethod.getParameterTypes();
        // 注解RedisIndexStart、RedisIndexEnd每个方法只能有一个并且每个注解的类型必须是Long
        String redisIndexException = "Class(" + pointMethod.getDeclaringClass().getName() + ") method(" + pointMethod.getName() +
                ") annotation(RedisIndexStart) and annotation(RedisIndexEnd) each must be unique and paramter type must be" +
                " one of Long、long、Integer、int!";
        Long redisIndexStart = null;
        Long redisIndexEnd = null;
        for (int index = 0; index < argumentAnnotationArray.length; index++) {
            Annotation[] argumentAnnotations = argumentAnnotationArray[index];
            for (Annotation argumentAnnotation : argumentAnnotations) {
                Class<?> argumentAnnotationType = argumentAnnotation.annotationType();
                if (argumentAnnotationType.equals(RedisIndexStart.class) || argumentAnnotationType.equals(RedisIndexEnd.class)) {
                    boolean isInvalidAnnotation = (argumentAnnotationType.equals(RedisIndexStart.class) && redisIndexStart != null)
                            || (argumentAnnotationType.equals(RedisIndexEnd.class) && redisIndexEnd != null);
                    if (isInvalidAnnotation) {
                        throw new RedisRuntimeException(redisIndexException);
                    }

                    Object argument = methodInvocation.getArguments()[index];
                    if (argument != null) {
                        if (Integer.class.equals(argumentTypes[index]) || int.class.equals(argumentTypes[index])) {
                            if (argumentAnnotationType.equals(RedisIndexStart.class)) {
                                redisIndexStart = (long) (int) argument;
                            } else {
                                redisIndexEnd = (long) (int) argument;
                            }
                        } else if (Long.class.equals(argumentTypes[index]) || long.class.equals(argumentTypes[index])) {
                            if (argumentAnnotationType.equals(RedisIndexStart.class)) {
                                redisIndexStart = (long) argument;
                            } else {
                                redisIndexEnd = (long) argument;
                            }
                        } else {
                            throw new RedisRuntimeException(redisIndexException);
                        }
                    }
                    break;
                }
            }
        }
        return new RedisIndexMap(redisIndexStart, redisIndexEnd);
    }

    /**
     * 获取当前方法执行结果
     */
    Object proceed(MethodInvocation methodInvocation) throws RedisInterceptorException {
        try {
            return methodInvocation.proceed();
        } catch (Throwable throwable) {
            Method method = methodInvocation.getMethod();
            throw new RedisInterceptorException("Class({}) method({}) execute failure!",
                    method.getDeclaringClass().getName(), method.getName(), throwable);
        }
    }
}